拒绝千篇一律,这套Go错误处理的完整解决方案值得一看!

您所在的位置:网站首页 golang error类型 拒绝千篇一律,这套Go错误处理的完整解决方案值得一看!

拒绝千篇一律,这套Go错误处理的完整解决方案值得一看!

2024-01-19 09:33| 来源: 网络整理| 查看: 265

导语 | 在使用Go开发的后台服务中,对于错误处理,一直以来都有多种不同的方案,本文探讨并提出一种从服务内到服务外的一个统一的传递、返回和回溯的完整方案,抛砖引玉,希望与大家一起讨论分享。

一、问题提出

在后台开发中,针对错误处理,有三个维度的问题需要解决:

函数内部的错误处理: 这是一个函数在执行过程中遇到各种错误时的错误处理。这是一个语言级的问题。

函数/模块的错误信息返回: 一个函数在操作错误之后,要怎么将这个错误信息优雅地返回,方便调用方(也要优雅地)处理。这也是一个语言级的问题。

服务/系统的错误信息返回: 微服务/系统在处理失败时,如何返回一个友好的错误信息,依然是需要让调用方优雅地理解和处理。这是一个服务级的问题,适用于任何语言。

二、函数内部的错误处理

一个面向过程的函数,在不同的处理过程中需要handle不同的错误信息;一个面向对象的函数,针对一个操作所返回的不同类型的错误,有可能需要进行不同的处理。此外,在遇到错误时,也可以使用断言的方式,快速中止函数流程,大大提高代码的可读性。

在许多高级语言中都提供了try...catch的语法,函数内部可以通过这种方案,实现一个统一的错误处理逻辑。而即便是C这种“中级语言”虽然没有,但是程序员也可以使用宏定义的方式,来实现某种程度上的错误断言。但是,对于Go的情况就比较尴尬了。

(一)Go的错误断言

我们先来看断言,我们的目的是,仅使用一行代码就能够检查错误并终止当前函数。由于没有throw,没有宏,如果要实现一行断言,有两种方法。

第一种是把if的错误判断写在一行内,比如:

if err != nil { return err }

第二种方法是借用panic函数,结合recover来实现:

func SomeProcess() (err error) defer func() { if e := recover(); e != nil { err = e.(error) } }() assert := func(cond bool, f string, a ...interface{}) { if !cond { panic(fmt.Errorf(f, a...)) } } // ... err = DoSomething() assert(err == nil, "DoSomething() error: %w", err) // ... }

这两种方法都值得商榷。

首先,将if写在同一行内的问题有:

这种写法,虽然理论上符合Go的代码规范,但是在实操中,花括号不换行这一点还是有点争议的,笔者在实际代码中也很少见到过。

不够直观,而且在花括号中也不方便写其他语句,原因是Go的规范中强烈不建议使用;来分隔代码语句(if 判断除外)

至于第二种方法,我们要分情况看:

首先panic的设计原意,是在当程序或协程遇到严重错误,完全无法继续运行下去的时候,才会调用(比如段错误、共享资源竞争错误)。这相当于Linux中FATAL级别的错误日志。仅仅用来进行普通的错误处理(ERROR级别),杀鸡用牛刀了。 panic调用本身,相比于普通的业务逻辑,的系统开销是比较大的。而错误处理这种事情,可能是常态化逻辑,频繁的panic-recover操作,也会大大降低系统的吞吐。

不过使用panic来断言的方案,虽然在业务逻辑中基本上不用,但在测试场景下则是非常常见的。测试嘛,用牛刀有何不可?稍微大一点的系统开销也没啥问题。对于Go来说,非常热门的单元测试框架goconvey就是使用panic机制来实现单元测试中的断言,用的人都说好。

综上,在Go中,对于业务代码,笔者不建议采用断言,遇到错误的时候建议还是老老实实采用这种格式:

if err := DoSomething(); err != nil { // ... }

而在单测代码中,则完全可以大大方方地采用类似于goconvey之类基于panic机制的断言。

(二)Go的try...catch

众所周知Go是没有try...catch的,而且从官方的态度来看,短时间内也没有考虑的计划。但程序员有这个需求呀。笔者采用的方法,是将需要返回的err变量在函数内部全局化,然后结合defer统一处理:

func SomeProcess() (err error) { // 0 } // hash 函数可以自定义 func hash(s string) uint64 { h := md5.Sum([]byte(s)) u := binary.BigEndian.Uint32(h[0:16]) return uint64(u ; 0xFFFFF) }

当然这种方案也有局限性,笔者能想到的是需要注意以下两点:

生成error时要避免记录随机数据、不可重放数据、千人千面的数据,比如说时间、账户号、流水ID等等信息,尽可能使用户进行统一操作时,能够生成相同的错误码。 由于数字1和字母I、数字0和字母O很类似,因此需要进行统一转换,避免出现歧义。这就是为什么在Err2Hashcode中,对hash结果encode之后要重新decode一次再返回的原因。

此外,笔者需要再强调的是:在开发中,针对各种不同的、正式的错误code和message用例依然需要完整覆盖,尽可能通过已有的code-message机制将足够清晰的信息告知主调方。这种hashcode的错误代码生成方法,仅适用于错误用例遗漏、或者是快递迭代过程中,用于发现和调试遗漏的错误用例的临时方案。

作者简介

张敏

腾讯高级后台工程师。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3